package it.unipg.bipod.dataAccess;

import java.sql.*;
import java.beans.*;

import it.unipg.bipod.dataModel.*;

import java.util.Vector;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

/**
 * DBLocalDataManager fornisce una implementazione di LocalDataManger che gestisce i
 * dati locali di BiPoD tramite database relazionale.<br>
 * La connessione al database viene presa dalla DataSource di Glassfish specificata
 * nel campo "dataSource" delle impostazioni passate al metodo {@code connect()}.<br>
 * Tutti i metodi di DBLocalDataManager lanciano una DataAccessException se riscontrano
 * un qualche errore in fase di accesso al database.<br>
 * Gli accessi al database vengono eseguiti tramite PreparedStatements: la prima volta
 * che un'operazione viene eseguita si crea un nuovo PreparedStatement che viene poi
 * salvato in una cache dalla quale viene recuperato ogni volta che quell'operazione
 * va eseguita nuovamente.
 * 
 * @author Lorenzo Porzi
 * @see LocalDataManager
 *
 */
class DBLocalDataManager implements LocalDataManager {
	private Connection connection; // Connessione al database
	private Hashtable<String, PreparedStatement> statements; // Tabella di prepared statements

	/**
	 * Crea un DBLocalDataManager. Prima di essere utilizzato è necessario iniziallizzare
	 * la connesione al database tramite il metodo {@code connect()}.
	 */
	public DBLocalDataManager() {		
		// Crea l'Hashtable di prepared statements
		statements = new Hashtable<String, PreparedStatement>();
	} 
	
	/**
	 * Si connette al database sulla base delle impostazioni passate come argomento.<br>
	 * In particolare l'argomento deve contenere un campo "dataSource" con il nome
	 * del DataSource a cui accedere.
	 * 
	 * @param settings Un {@code Properties} di impostazioni
	 * @throws DataAccessException
	 */
	@Override
	public void connect(Properties settings) throws DataAccessException {
		// Apre la connessione al database
		try {
			InitialContext initialContext = new InitialContext();
			DataSource dataSource = (DataSource) initialContext.lookup(settings.getProperty("dataSource"));
			connection = dataSource.getConnection();
			
		} catch (NamingException e) {
			// Errore con il recupero del DataSource
			throw new DataAccessException(e);
			
		} catch (SQLException e) {
			// Errore nell'apertura della connessione
			throw new DataAccessException(e);
			
		}
	}
	
	@Override
	public boolean getEntityFromId(Object entity, int id) throws DataAccessException {
		try {
			BeanInfo info = Introspector.getBeanInfo(entity.getClass(), Object.class);
			String entityName = entity.getClass().getSimpleName();
			PreparedStatement statement;
			
			// Ricava le proprietà
			PropertyDescriptor[] properties = info.getPropertyDescriptors();
			
			// Crea la query
			String sqlQuery = "SELECT * FROM " + entityName + " WHERE id" + entityName + " = ?";
			if (!statements.containsKey(entityName + "FromID"))
				statement = addStatement(entityName + "FromID", sqlQuery);
			else
				statement = statements.get(entityName + "FromID");			
			statement.setInt(1, id);
			
			// Esegue la query
			ResultSet resultSet = statement.executeQuery();
			
			// Estrae i dati
			if (resultSet.next()) {
				for (PropertyDescriptor property : properties)
					property.getWriteMethod().invoke(entity, resultSet.getObject(property.getName()));
				
				// Chiude il ResultSet
				resultSet.close();
				
				return true;
			}
			
			// Chiude il ResultSet
			resultSet.close();
			return false;
		} catch (Exception e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public boolean insertEntity(Object entity) throws DataAccessException {
		try {
			BeanInfo info = Introspector.getBeanInfo(entity.getClass(), Object.class);
			String entityName = entity.getClass().getSimpleName();
			PreparedStatement statement;
			
			// Ricava le proprietà
			PropertyDescriptor[] properties = info.getPropertyDescriptors();
			
			// Controlla se lo statement già esiste
			if (statements.containsKey("insert" + entityName))
				statement = statements.get("insert" + entityName);
			else {
				// Genera lo statement
				String sqlQuery = "INSERT INTO " + entityName + " (";
				for (PropertyDescriptor property : properties) {
					if (property.getName().equals("id" + entityName))
						continue; // Salto l'id
					
					sqlQuery += property.getName() + ",";
				}
				sqlQuery = sqlQuery.substring(0, sqlQuery.length() - 1) + ") VALUES (";
				
				for (int i = 0; i < properties.length - 1; ++i)
					sqlQuery += "?,";				
				sqlQuery = sqlQuery.substring(0, sqlQuery.length() - 1) + ")";
				
				statement = addStatement("insert" + entityName, sqlQuery, "id" + entityName);
			}
			
			// Riempe lo statement
			int i = 0;
			for (PropertyDescriptor property : properties) {
				if (property.getName().equals("id" + entityName))
					continue; // Salto l'id
				
				i++;
				
				statement.setObject(i, property.getReadMethod().invoke(entity));
			}
			
			// Eseguo la query
			int resultSize = statement.executeUpdate();
			ResultSet keys = statement.getGeneratedKeys();
			
			if (resultSize > 0) {
				keys.next();				
				int id = keys.getInt(1); // Chiave del nuovo record
				
				for (PropertyDescriptor property : properties)
					if (property.getName().equals("id" + entityName)) {
						property.getWriteMethod().invoke(entity, id); // Aggioro
						break;
					}
				
				// Chiude il ResultSet
				keys.close();
				
				return true;
			} else
				return false;
		} catch (Exception e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}		
	}
	
	@Override
	public boolean updateEntity(Object entity) throws DataAccessException {
		try {
			BeanInfo info = Introspector.getBeanInfo(entity.getClass(), Object.class);
			String entityName = entity.getClass().getSimpleName();
			PreparedStatement statement;
			
			// Ricava le proprietà
			PropertyDescriptor[] properties = info.getPropertyDescriptors();
			PropertyDescriptor idProperty = null;
			for (PropertyDescriptor property : properties)
				if (property.getName().equals("id" + entityName)) {
					idProperty = property; // Salvo l'id
					break;
				}
			
			// Controlla se lo statement già esiste
			if (statements.containsKey("update" + entityName))
				statement = statements.get("update" + entityName);
			else {
				// Genera lo statement
				String sqlQuery = "UPDATE " + entityName + " SET ";
				for (PropertyDescriptor property : properties) {
					if (property == idProperty)
						continue; // Salto l'id
					
					sqlQuery += property.getName() + " = ?,";
				}
				sqlQuery = sqlQuery.substring(0, sqlQuery.length() - 1) + " WHERE id" + entityName + " = ?";
				
				statement = addStatement("update" + entityName, sqlQuery);
			}
			
			// Riempe lo statement
			int i = 0;
			for (PropertyDescriptor property : properties) {
				if (property == idProperty)
					continue; // Salto l'id
				
				i++;
				
				statement.setObject(i, property.getReadMethod().invoke(entity));
			}
			
			// Inserisce nello statement anche l'id
			statement.setInt(properties.length, (Integer)idProperty.getReadMethod().invoke(entity));
			
			// Esegue la query
			int resultSize = statement.executeUpdate();
			
			if (resultSize > 0)				
				return true;
			else
				return false;
		} catch (Exception e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public boolean removeEntity(Object entity) throws DataAccessException {
		try {
			BeanInfo info = Introspector.getBeanInfo(entity.getClass(), Object.class);
			String entityName = entity.getClass().getSimpleName();
			PreparedStatement statement;
			
			// Ricava le proprietà
			PropertyDescriptor[] properties = info.getPropertyDescriptors();
			PropertyDescriptor idProperty = null;
			for (PropertyDescriptor property : properties)
				if (property.getName().equals("id" + entityName)) {
					idProperty = property; // Salvo l'id
					break;
				}
			
			// Controlla se lo statement già esiste
			if (statements.containsKey("remove" + entityName))
				statement = statements.get("remove" + entityName);
			else {
				// Genera lo statement
				String sqlQuery = "DELETE FROM " + entityName + " WHERE ";
				sqlQuery += "id" + entityName + " = ?";
				
				statement = addStatement("remove" + entityName, sqlQuery);
			}
			
			// Compila lo statement
			statement.setInt(1, (Integer)idProperty.getReadMethod().invoke(entity));
			
			// Esegue lo statement
			int resultSize = statement.executeUpdate();
			
			if (resultSize > 0)
				return true;
			else
				return false;
		} catch (Exception e) {
			// C'è stato un errore
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Object[] getAll(Class<?> entityClass) throws DataAccessException {
		try {
			BeanInfo info = Introspector.getBeanInfo(entityClass, Object.class);
			String entityName = entityClass.getSimpleName();
			PreparedStatement statement;
			
			// Ricava le proprietà
			PropertyDescriptor[] properties = info.getPropertyDescriptors();
			
			// Crea la query
			if (!statements.containsKey("all" + entityName)) {
				String sqlQuery = "SELECT * FROM " + entityName;
				statement = addStatement("all" + entityName, sqlQuery);
			} else
				statement = statements.get("all" + entityName);
			
			// Esegue la query
			ResultSet resultSet = statement.executeQuery();
			
			// Estrae i dati
			Vector<Object> entities = new Vector<Object>();
			while (resultSet.next()) {
				Object entity = entityClass.newInstance();
				
				for (PropertyDescriptor p : properties)
					p.getWriteMethod().invoke(entity, resultSet.getObject(p.getName()));
				
				// Aggiunge l'entità al vettore
				entities.add(entity);
			}
			
			// Chiude il ResultSet
			resultSet.close();
			
			return entities.toArray();
		} catch (Exception e) {
			// C'è stato un qualche intoppo
			throw new DataAccessException(e);
		}
	}
	
	/* -------------------------------------------------
	 * Recupero di Entità a partire dalle chiavi esterne
	 * ------------------------------------------------- */
	
	@Override
	public Insegnamento[] getInsegnamentoFromBando(int idBando) throws DataAccessException {
		try {
			PreparedStatement insegnamentoFromBando;
			Vector<Insegnamento> vectorInsegnamenti = new Vector<Insegnamento>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("insegnamentoFromBando"))
				insegnamentoFromBando = addStatement("insegnamentoFromBando", "SELECT * FROM Insegnamento WHERE Bando = ?");
			else
				insegnamentoFromBando = statements.get("insegnamentoFromBando");
			
			// Imposto i parametri
			insegnamentoFromBando.setInt(1, idBando);
			
			// Eseguo l'operazione
			ResultSet resultSet = insegnamentoFromBando.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati
				int idInsegnamento = resultSet.getInt("idInsegnamento");
				String nome = resultSet.getString("nome");
				String ssd = resultSet.getString("ssd");
				int cfu = resultSet.getInt("cfu");
				int ore = resultSet.getInt("ore");
				String compenso = resultSet.getString("compenso");
				int corso = resultSet.getInt("Corso");
				
				vectorInsegnamenti.add(new Insegnamento(idInsegnamento, nome, ssd, cfu, ore, compenso, corso, idBando));
			}
			resultSet.close();
			
			Insegnamento[] insegnamenti = new Insegnamento[vectorInsegnamenti.size()];
			vectorInsegnamenti.toArray(insegnamenti);
			
			return insegnamenti;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Insegnamento[] getInsegnamentoFromCorso(int idCorso) throws DataAccessException {
		try {
			PreparedStatement insegnamentoFromCorso;
			Vector<Insegnamento> vectorInsegnamenti = new Vector<Insegnamento>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("insegnamentoFromCorso"))
				insegnamentoFromCorso = addStatement("insegnamentoFromCorso", "SELECT * FROM Insegnamento WHERE Corso = ?");
			else
				insegnamentoFromCorso = statements.get("insegnamentoFromCorso");
			
			// Imposto i parametri
			insegnamentoFromCorso.setInt(1, idCorso);
			
			// Eseguo l'operazione
			ResultSet resultSet = insegnamentoFromCorso.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati
				int idInsegnamento = resultSet.getInt("IDInsegnamento");
				String nome = resultSet.getString("Nome");
				String ssd = resultSet.getString("SSD");
				int cfu = resultSet.getInt("CFU");
				int ore = resultSet.getInt("ore");
				String compenso = resultSet.getString("compenso");
				int bando = resultSet.getInt("Bando");
				
				vectorInsegnamenti.add(new Insegnamento(idInsegnamento, nome, ssd, cfu, ore, compenso, idCorso, bando));
			}
			resultSet.close();
			
			Insegnamento[] insegnamenti = new Insegnamento[vectorInsegnamenti.size()];
			vectorInsegnamenti.toArray(insegnamenti);
			
			return insegnamenti;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Operazione[] getOperazioneFromRegistrazione(int idRegistrazione) throws DataAccessException {
		try {
			PreparedStatement operazioneFromUtente;
			Vector<Operazione> vectorOperazioni = new Vector<Operazione>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("operazioneFromUtente"))
				operazioneFromUtente = addStatement("operazioneFromUtente", "SELECT * FROM Operazione WHERE Utente = ?");
			else
				operazioneFromUtente = statements.get("operazioneFromUtente");
			
			// Imposto i parametri
			operazioneFromUtente.setInt(1, idRegistrazione);
			
			// Eseguo l'operazione
			ResultSet resultSet = operazioneFromUtente.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati
				int idOperazione = resultSet.getInt("idOperazione");
				Timestamp dataEsecuzione = resultSet.getTimestamp("dataEsecuzione");
				String tipo = resultSet.getString("tipo");
				String descrizione = resultSet.getString("descrizione");
				String hash = resultSet.getString("hash");
				
				vectorOperazioni.add(new Operazione(idOperazione, dataEsecuzione, tipo, descrizione, idRegistrazione, hash));
			}
			resultSet.close();
			
			Operazione[] operazioni = new Operazione[vectorOperazioni.size()];
			vectorOperazioni.toArray(operazioni);
			
			return operazioni;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Registrazione getRegistrazioneFromUtente(String utente) throws DataAccessException {
		try {
			PreparedStatement registrazioneFromUtente;
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("registrazioneFromUtente"))
				registrazioneFromUtente = addStatement("registrazioneFromUtente", "SELECT * FROM Registrazione WHERE Utente = ?");
			else
				registrazioneFromUtente = statements.get("registrazioneFromUtente");
			
			// Imposto i parametri
			registrazioneFromUtente.setString(1, utente);
			
			// Eseguo l'operazione
			ResultSet resultSet = registrazioneFromUtente.executeQuery();			
			if (resultSet.next()) {
				// Estraggo i dati
				int idRegistrazione = resultSet.getInt("idRegistrazione");
				Timestamp dataInserimento = resultSet.getTimestamp("dataInserimento");
				String ruolo = resultSet.getString("ruolo");
				boolean attiva = resultSet.getBoolean("attiva");
				
				resultSet.close();
				
				return new Registrazione(idRegistrazione, utente, dataInserimento, ruolo, attiva);
			}
			resultSet.close();
			
			return null;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Richiesta[] getRichiestaFromRegistrazione(int idRegistrazione) throws DataAccessException {
		try {
			PreparedStatement richiestaFromDocente;
			Vector<Richiesta> vectorRichieste = new Vector<Richiesta>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("richiestaFromDocente"))
				richiestaFromDocente = addStatement("richiestaFromDocente", "SELECT * FROM Richiesta WHERE Docente = ?");
			else
				richiestaFromDocente = statements.get("richiestaFromDocente");
			
			// Imposto i parametri
			richiestaFromDocente.setInt(1, idRegistrazione);
			
			// Eseguo l'operazione
			ResultSet resultSet = richiestaFromDocente.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati
				int idRichiesta = resultSet.getInt("idRichiesta");
				Timestamp dataRichiesta = resultSet.getTimestamp("dataRichiesta");
				String qualificaDocente = resultSet.getString("qualificaDocente");
				String ssdDocente = resultSet.getString("ssdDocente");
				int insegnamento = resultSet.getInt("insegnamento");
				
				vectorRichieste.add(new Richiesta(idRichiesta, dataRichiesta, qualificaDocente, ssdDocente, idRegistrazione, insegnamento));
			}
			resultSet.close();
			
			Richiesta[] richieste = new Richiesta[vectorRichieste.size()];
			vectorRichieste.toArray(richieste);
			
			return richieste;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Richiesta[] getRichiestaFromInsegnamento(int idInsegnamento) throws DataAccessException {
		try {
			PreparedStatement richiestaFromInsegnamento;
			Vector<Richiesta> vectorRichieste = new Vector<Richiesta>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("richiestaFromInsegnamento"))
				richiestaFromInsegnamento = addStatement("richiestaFromInsegnamento", "SELECT * FROM Richiesta WHERE Insegnamento = ?");
			else
				richiestaFromInsegnamento = statements.get("richiestaFromInsegnamento");
			
			// Imposto i parametri
			richiestaFromInsegnamento.setInt(1, idInsegnamento);
			
			// Eseguo l'operazione
			ResultSet resultSet = richiestaFromInsegnamento.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati
				int idRichiesta = resultSet.getInt("idRichiesta");
				Timestamp dataRichiesta = resultSet.getTimestamp("dataRichiesta");
				String qualificaDocente = resultSet.getString("qualificaDocente");
				String ssdDocente = resultSet.getString("ssdDocente");
				int docente = resultSet.getInt("docente");
				
				vectorRichieste.add(new Richiesta(idRichiesta, dataRichiesta, qualificaDocente, ssdDocente, docente, idInsegnamento));
			}
			resultSet.close();
			
			Richiesta[] richieste = new Richiesta[vectorRichieste.size()];
			vectorRichieste.toArray(richieste);
			
			return richieste;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	@Override
	public Registrazione[] getRegistrazioneFromInsegnamento(int idInsegnamento) throws DataAccessException {
		try {
			PreparedStatement docenteFromInsegnamento;
			Vector<Registrazione> vectorRegistrazioni = new Vector<Registrazione>();
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("docenteFromInsegnamento"))
				docenteFromInsegnamento = addStatement("docenteFromInsegnamento",
						"SELECT r.* FROM Registrazione r, Richiesta ri, Insegnamento i WHERE " +
						"i.idInsegnamento = ? AND ri.insegnamento = i.idInsegnamento AND ri.docente = r.idRegistrazione");
			else
				docenteFromInsegnamento = statements.get("docenteFromInsegnamento");
			
			// Imposto i parametri
			docenteFromInsegnamento.setInt(1, idInsegnamento);
			
			// Eseguo l'operazione
			ResultSet resultSet = docenteFromInsegnamento.executeQuery();			
			while (resultSet.next()) {
				// Estraggo i dati				
				vectorRegistrazioni.add(new Registrazione(
						resultSet.getInt("idRegistrazione"),
						resultSet.getString("utente"),
						resultSet.getTimestamp("dataInserimento"),
						resultSet.getString("ruolo"),
						resultSet.getBoolean("attiva")));
			}
			resultSet.close();
			
			Registrazione[] registrazioni = new Registrazione[vectorRegistrazioni.size()];
			vectorRegistrazioni.toArray(registrazioni);
			
			return registrazioni;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	/* ------------
	 * Altri metodi
	 * ------------ */
	
	@Override
	public Corso getCorsoFromNome(String nome) throws DataAccessException {
		try {
			PreparedStatement esisteCorso;
			
			// Se il prepared statement non esiste lo aggiungo alla tabella
			if (!statements.containsKey("esisteCorso"))
				esisteCorso = addStatement("esisteCorso", "SELECT * FROM Corso WHERE nome LIKE ?");
			else
				esisteCorso = statements.get("esisteCorso");
			
			// Imposto i parametri
			esisteCorso.setString(1, nome);
			
			// Eseguo l'operazione
			ResultSet resultSet = esisteCorso.executeQuery();
			
			// Estrae il risultato
			if (resultSet.next()) {
				// Abbiamo trovato un corso
				Corso corso = new Corso(
						resultSet.getInt("idCorso"),
						resultSet.getString("nome"));
				
				resultSet.close();
				
				return corso;
			}
			resultSet.close();
			
			// Nessun corso con questo nome
			return null;
		} catch (SQLException e) {
			// C'è stato un qualche intoppo			
			throw new DataAccessException(e);
		}
	}
	
	/* ---------------------------
	 * Funzioni private di utilità
	 * --------------------------- */
	
	/**
	 * Aggiunge un prepared statement alla tabella.
	 * 
	 * @param nome La key da associare allo statement.
	 * @param sql Il comando sql del prepared statement.
	 * @return il PreparedStatement aggiunto.
	 */
	private PreparedStatement addStatement(String nome, String sql) throws SQLException {
		PreparedStatement statement = connection.prepareStatement(sql);
		
		statements.put(nome, statement);
		
		return statement;
	}
	
	/**
	 * Aggiunge un prepared statement il cui risultato ha un id autogenerato.
	 * 
	 * @param nome La key da associare allo statement.
	 * @param sql Il comando sql del prepared statement.
	 * @param idName Il nome dell'id autogenerato.
	 * @return il PreparedStatement aggiunto.
	 */
	private PreparedStatement addStatement(String nome, String sql, String idName) throws SQLException {
		String[] id = {idName};
		PreparedStatement statement = connection.prepareStatement(sql, id);
		
		statements.put(nome, statement);
		
		return statement;
	}
}